package cbbx_sm.probabilistic_model;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;

import cbbx_sm.parser.BoundingBox;
import cbbx_sm.parser.CameraData;
import cbbx_sm.parser.Entity;
import cbbx_sm.parser.Frame;
import cbbx_sm.utils.ExperimentManager;
import cbbx_sm.utils.Timestamp;

/**
 * This object gives methods for calculating 
 * probability model of bounding boxes in a BBoxData structure.
 * 
 * @author Alessio Della Motta - University of California, Irvine
 *         Ronen Vaisenberg - University of California, Irvine
 */
public class ProbabilisticModel implements Serializable{
	
	private static final long serialVersionUID = 1L;
	public int timeWindow = -1;
	private double independentTimeCorrelationProbabilityThreshold = 0.0;
	private double[] aPrioriProbabilities = null;
	private double[][][][] independentTimeCorrelationProbabilities = null;
	// NOTE: it is important that we use a linked hash map so that the order in which 
	//       we iterate of the clusters when we project the model is predictable.
	private LinkedHashMap<String, Integer> clustersIndex = new LinkedHashMap<String, Integer>();
	private List<Cluster> fusedClusters = null;
	
	public double getIndependentTimeCorrelationProbabilityThreshold() {
		return independentTimeCorrelationProbabilityThreshold;
	}

	public void setIndependentTimeCorrelationProbabilityThreshold(
			double independentTimeCorrelationProbabilityThreshold) {
		this.independentTimeCorrelationProbabilityThreshold = independentTimeCorrelationProbabilityThreshold;
	}

	public List<Cluster> getFusedClusters(){
		return fusedClusters;
	}
	
	private ProbabilisticModel(){
		super();
	}
	
	public ProbabilisticModel(double[] aPrioriProbabilities, double[][][][] independentTimeCorrelationProbabilities,
			LinkedHashMap<String, Integer> clustersIndex, List<Cluster> fusedClusters, int timeWindow){
		this.aPrioriProbabilities = aPrioriProbabilities;
		this.independentTimeCorrelationProbabilities = independentTimeCorrelationProbabilities;
		this.clustersIndex = clustersIndex;
		this.fusedClusters = fusedClusters;
		this.timeWindow = timeWindow;
	}

	public double getAPrioriProbability(Cluster c){
		double prob = aPrioriProbabilities[clustersIndex.get(c.getUniqueId())];	
		return prob;
	}
	
	/**
	 * Computes the transition probability between two states and time offset,
	 * taking into account the probability of seeing motion given motion as well as
	 * seeing motion given no motion.
	 * 
	 * @param timeOffset the time offset between the two events.
	 * @param probabilityFrom the probability of motion in the original event.
	 * @param from the cluster from which we start.
	 * @param to the cluster to which we transition.
	 * @return 1 - (1-p(motion|motion) * (1-p(motion|no-motion))
	 */
	public double getIndependentTimeCorrelationProbability(
			int timeOffset, double probabilityFrom, Cluster from, Cluster to){
		double prob = 1 - probabilityFrom * independentTimeCorrelationProbabilities[1][timeOffset]
		    [clustersIndex.get(from.getUniqueId())][clustersIndex.get(to.getUniqueId())];
		if (ExperimentManager.useFromZeroTransitionProb) {
			prob *= 1 - (1-probabilityFrom) * independentTimeCorrelationProbabilities[0][timeOffset]
			    [clustersIndex.get(from.getUniqueId())][clustersIndex.get(to.getUniqueId())];			
		}

		prob = 1 - prob;
		if (prob < this.independentTimeCorrelationProbabilityThreshold) {
			return 0;
		} else {
			return prob;
		}
	}
	
	public ProbabilisticModel(List<CameraData> data, List<Cluster> clst, int timeWindow){
		this.fusedClusters = clst;
		this.timeWindow = timeWindow;
		
		// Set the motion stream based on the motion observed.
		Timestamp initialTimestamp = getGlobalInitialTimeStamp(data);
		Timestamp finalTimestamp = getGlobalFinalTimeStamp(data);
		int timeSpan = (int) (Timestamp.subtractTimestamps(finalTimestamp, initialTimestamp) + 1);
		boolean [][] motionStream = new boolean[clst.size()][timeSpan];
		for (int i = 0; i < clst.size(); i++) {
			Cluster c = clst.get(i);
			clustersIndex.put(clst.get(i).getUniqueId(), i);
			CameraData relevantCamera = null;
			
			// Set the current camera.
			for (CameraData camData : data) {
				if (camData.getCameraId().compareTo(c.getCameraId()) == 0) {
					relevantCamera = camData;
					break;
				}
			}

			// Load the motion stream vectors.
			List<Frame> frames = relevantCamera.getFrames();
			int fid = 0;
			for (int t = 0; t < timeSpan; t++) {
				while ((fid < frames.size()) && 
						frames.get(fid).getTimestamp().compareTo(Timestamp.getTimeStampInFuture(initialTimestamp, t)) < 0) {
					fid++;
				}
				if (fid == frames.size()) {
					break;
				}
				if (frames.get(fid).getTimestamp().compareTo(Timestamp.getTimeStampInFuture(initialTimestamp, t)) == 0) {
					motionStream[i][t] = isClusterContainingABBox(clst, c, frames.get(fid));
				}
			}
		}

		this.aPrioriProbabilities = 
			computeAPrioriProbabilities(motionStream);
		this.independentTimeCorrelationProbabilities = 
			computeIndependentTimeCorrelationProbabilities(motionStream, timeWindow);
	}
	
	public static Timestamp getGlobalFinalTimeStamp(List<CameraData> data) {
		Timestamp res = data.get(0).getFinalTimestamp();
		for (CameraData c : data) {
			// Find the maximal timestamp.
			if (c.getFinalTimestamp().compareTo(res) > 0) {
				res = c.getFinalTimestamp();
			}
		}
		return res;
	}

	public static Timestamp getGlobalInitialTimeStamp(List<CameraData> data) {
		Timestamp res = data.get(0).getInitialTimestamp();
		for (CameraData c : data) {
			// Find the minimal timestamp.
			if (c.getInitialTimestamp().compareTo(res) < 0) {
				res = c.getInitialTimestamp();
			}
		}
		return res;
	}

	public int getClusterIndex(Cluster cluster){
		return clustersIndex.get(cluster.getUniqueId());
	}
	
	public int getClusterIndex(String uniqueId) {
	  return clustersIndex.get(uniqueId);
	}
	
	/**
	 * Compute the apriori probabilities to contain a bounding box
	 * of each cluster. 
	 * They are different if you compute data from multiple cameras,
	 * because the number of bounding box increases.
	 * 
	 * @param motionStream two dimension array representing the motion  vectors of all clusters.
	 * @return 
	 */
	private static double[] computeAPrioriProbabilities(boolean [][] motionStream){
		int numberOfClusters = motionStream.length;
		double[] aPrioriProbabilities = new double[numberOfClusters];
		int[] clustersHits = new int[numberOfClusters]; 
		
		// Count the number of events seen in that cluster.
		for (int i = 0; i < numberOfClusters; i++) {
			for (int t = 0; t < motionStream[0].length; t++) {
				if (motionStream[i][t])
					clustersHits[i] ++;	
			}		
		}
		
		// compute the a priori probability as the frac of events to total
		for (int i = 0; i < aPrioriProbabilities.length; i++)
			aPrioriProbabilities[i] = ((double)clustersHits[i]) / ((double)motionStream[0].length);
		
		return aPrioriProbabilities;
	}
	
	
	private static double[][][][] computeIndependentTimeCorrelationProbabilities(boolean [][] motionStream, int timeWindow){
		return computeIndependentTimeCorrelationProbabilities(motionStream, 0.0, timeWindow);
	}
	
	/**
	 * Compute the probability that, given a box in cluster c1 at time t1,
	 * a bounding box will be in cluster c2 at time t2.
	 * 
	 * @param data
	 * @return 
	 */
	protected static double[][][][] computeIndependentTimeCorrelationProbabilities(
			boolean [][] motionStream, double threshold, int timeWindow){
		// Total times that current cluster contains a bounding box.
		int numberOfClusters = motionStream.length;
		int[] totalEventsInCamera = new int[numberOfClusters];
		for (int timeRunner = 0; timeRunner < motionStream[0].length; timeRunner++) {
			for (int index = 0; index < numberOfClusters; index++){
				if (motionStream[index][timeRunner]) {
					totalEventsInCamera[index]++;	
				}				
			}
		}
		double[][][][] independentTimeCorrelationProbabilities = new double[2][timeWindow + 1][numberOfClusters][numberOfClusters];
		for (int indexTo = 0; indexTo < numberOfClusters; indexTo++){		
			// Compute the correlations.
			for (int motion = 0; motion <= 1; motion ++) {
				for (int t = 0; t <= timeWindow; t++){
					for (int indexFrom = 0; indexFrom < numberOfClusters; indexFrom++){
						int numberOfConditionalHits = 0;
						for (int timeRunner = 0; timeRunner < motionStream[0].length - t; timeRunner++) {
							if (motionStream[indexTo][timeRunner + t]) {
								// If motion == 1 we must have true in the motion bit
								if ((motionStream[indexFrom][timeRunner] && (motion == 1) )) {							
									numberOfConditionalHits++;	
								}
								// if motion == 0 we must have false in the motion bit.
								if ((motionStream[indexFrom][timeRunner] == false) 
										&& (motion == 0) ) {							
									numberOfConditionalHits++;	
								}
							}
						}
						double baseEvents = motion == 1 ? totalEventsInCamera[indexFrom] : (motionStream[0].length - totalEventsInCamera[indexFrom]);
						independentTimeCorrelationProbabilities[motion][t][indexFrom][indexTo] = 
							((double) numberOfConditionalHits) / baseEvents;		
					}
				}				
			}
		}
		return independentTimeCorrelationProbabilities;
	}

	/**
	 * Verify if a cluster contains at least a bounding box of
	 * the frame passed as parameter.
	 * 
	 * @param c - cluster
	 * @param f - frame
	 * @return if cluster c contains at least a bbox of frame f.
	 */
	public boolean isClusterContainingABBox(List<Cluster> allClusters, Cluster c, Frame f){
		boolean clusterContainsABBox = false;
		String cameraId = f.getCameraId();
		
		if (c == null) return false;
		if (!c.getCameraId().equals(f.getCameraId())) return false;
		
		for (Entity e : f.getEntities()){
			if (getOwningCluster(allClusters, e.getBoxes()[ExperimentManager.boxLocationId], cameraId) == c){
				clusterContainsABBox = true;
				break;
			}
		}
		
		return clusterContainsABBox;
	}
		
	/**
	 * Return the cluster that a bounding box belongs to.
	 * 
	 * @param box
	 * @return parent cluster
	 */
	public static Cluster getOwningCluster (List<Cluster> clusters, BoundingBox box, String cameraId){
		if (box == null) return null;
		
		double distance = Double.POSITIVE_INFINITY;
		Cluster parentCluster = null;
		
		for (Cluster c : clusters){
			
			if (!c.getCameraId().equals(cameraId)) continue;
			
			double currDist = c.getClusterDistance(box);
			if (currDist < distance) {
				distance = currDist;
				parentCluster = c;
			}
		}
		
		return parentCluster;
	}
	
	public String aPrioriString(){
		StringBuffer str = new StringBuffer();
		
		if (aPrioriProbabilities != null){
			str.append("A PRIORI PROBABILITIES:\n");
			for (int i = 0; i < aPrioriProbabilities.length; i++)
				str.append("\tProb Cluster " + i + ": " + getAPrioriProbability(fusedClusters.get(i)) + "\n");
		}
		else str.append("A PRIORI PROBABILITIES NOT PRESENT!\n");
		
		return str.toString();
	}
	
	@Override
	public String toString(){
		StringBuffer str = new StringBuffer();
		
		str.append("Correlation Probabilities:"+'\n');
		if (independentTimeCorrelationProbabilities != null){
			for (int motion = 0; motion <= 1; motion++) {
				for (int t = 0; t < timeWindow; t++){
					str.append("t="+t+"\n");
				for (int index1 = 0; index1 < fusedClusters.size(); index1++){
					for (int index2 = 0; index2 < fusedClusters.size(); index2++){
							double probability = getIndependentTimeCorrelationProbability(
									t, motion, fusedClusters.get(index1), fusedClusters.get(index2));
							str.append("" + probability + "\t");
						}
						str.append("\n");
					}
					str.append("\n");
				}	
			}			
		}else str.append("CORRELATION PROBABILITIES NOT PRESENT!\n");
		
		str.append("Apriori Probabilities:"+'\n');
		if (aPrioriProbabilities != null){	
			for (int index1 = 0; index1 < fusedClusters.size(); index1++){
				double probability = getAPrioriProbability(fusedClusters.get(index1));
				str.append("" + probability + "\t");
			}
		}else str.append("APRIORI PROBABILITIES NOT PRESENT!\n");
		
		// Print the 1 second correlations greater than p = 0.1.
		for (int index1 = 0; index1 < fusedClusters.size(); index1++){
			for (int index2 = 0; index2 < fusedClusters.size(); index2++){
				int t = 1;
				int motion = 1;
				double probability = getIndependentTimeCorrelationProbability(
						t, motion, fusedClusters.get(index1), fusedClusters.get(index2));
				if (probability > 0.1) {
					str.append(String.format("%s->%s : %f\n",fusedClusters.get(index1), fusedClusters.get(index2), probability));					
				}
			}
			str.append("\n");
		}
		
		return str.toString();
	}
	
	// XXX: You should create a new copy of the array...
	public void setIndependentTimeCorrelationProbabilities(
			double[][][][] independentTimeCorrelationProbabilities) {
		this.independentTimeCorrelationProbabilities = independentTimeCorrelationProbabilities;
	}
	
	
	
	public static void writeDown(Object object, String filename){
		try { 
			// Serialize to a file 
			ObjectOutput out = new ObjectOutputStream(new FileOutputStream("output/probs/" + filename)); 
			out.writeObject(object); 
			out.close(); 
		} 
		catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public static ProbabilisticModel readUp(String filename){
		ProbabilisticModel model = null;
		File file = null;
		try { 
			// Deserialize from a file 
			file = new File("output/probs/" + filename); 
			ObjectInputStream in = new ObjectInputStream(new FileInputStream(file)); 
			// Deserialize the object javax.swing.JButton 
			model = (ProbabilisticModel) in.readObject();
			in.close(); 
		} 
		catch (ClassNotFoundException e) { 
			e.printStackTrace();
		} 
		catch (IOException e) { 
			e.printStackTrace();
		} 
		catch (ClassCastException e) {
			// The implementation was changed.
			file.delete();
			return null;
		}
		
		
		return model;
	}

	public void setThreshold(double threshold) {
		this.independentTimeCorrelationProbabilityThreshold = threshold;
	}

	public double getThreshold() {
		return independentTimeCorrelationProbabilityThreshold;
	}
	
	public HashMap<String, Integer> getClustersIndex() {
		return clustersIndex;
	}
}
